package uk.ac.nott.cs.g53dia.bee;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import sun.security.action.GetBooleanAction;
import uk.ac.nott.cs.g53dia.Action;
import uk.ac.nott.cs.g53dia.Bee;
import uk.ac.nott.cs.g53dia.Cell;
import uk.ac.nott.cs.g53dia.CommunicateAction;
import uk.ac.nott.cs.g53dia.CreateNestAction;
import uk.ac.nott.cs.g53dia.EmptyCell;
import uk.ac.nott.cs.g53dia.Environment;
import uk.ac.nott.cs.g53dia.Flower;
import uk.ac.nott.cs.g53dia.FollowQueenAction;
import uk.ac.nott.cs.g53dia.HarvestAction;
import uk.ac.nott.cs.g53dia.MoveAction;
import uk.ac.nott.cs.g53dia.MoveTowardsAction;
import uk.ac.nott.cs.g53dia.Nest;
import uk.ac.nott.cs.g53dia.Point;
import uk.ac.nott.cs.g53dia.RestAction;
import uk.ac.nott.cs.g53dia.UnseenCell;
import utils.ComparatorFactory;
import utils.Constants;
import utils.General;
import utils.RandomGenerator;
import utils.Simulation;

public class MyBee extends Bee {

  // Location of the most recently sensed flower - null if bee has yet to
  // sense a flower
  Point last_fl_loc = null;

  private int numBees = 8;
  private Map<Point, Cell> cells;
  private List<Set<Point>> flowersOfType;
  private List<Map<Long, Integer>> samples;
  private int[] hvntComm = null;
  private float[] weights;
  private Queue<Action> plan;
  private Action previousAction;
  private int previousEnergy;
  // Location of the bee's nest - null if the bee has yet to create a nest
  private Point nestLocation = null;
  private boolean nestCreated = false;
  private Point lastQueenSeen = null;
  private boolean initialScouting = true;
  private int day;
  private long lastTimestep;
  private static final float NUM_OF_FLOWERS_FACOR = 0.3f;
  private static final float DISTANCE_FACOR = 0.7f;
  private static final float ENOUGH_NUMBER_BONUS = 1f;
  private static final float ENOUGH_NUMBER_OF_ALL_BONUS = 0.8f;

  private BeeActionPlanner planner;
  private BeeYieldApproximator approximator;

  public static float SCOUT_HARVESTER_RATIO = 0.6f;
  private Role role;

  public MyBee(int id) {
    super(id);
    day = 0;
    if (id == 0) {
      role = Role.QUEEN;
    } else {
      role = Role.NONQUEEN;
      // role = RandomGenerator.getRandomFloat() < SCOUT_HARVESTER_RATIO ?
      // Role.SCOUT
      // : Role.HARVESTER;
    }
    cells = new HashMap<Point, Cell>();
    flowersOfType = new ArrayList<Set<Point>>(Flower.NUM_TYPES);
    for (int i = 0; i < Flower.NUM_TYPES; i++) {
      flowersOfType.add(new HashSet<Point>());
    }
    weights = new float[Flower.NUM_TYPES];
    for (int i = 0; i < Flower.NUM_TYPES; i++) {
      weights[i] = 1f;
    }
    samples = new ArrayList<Map<Long, Integer>>(Flower.NUM_TYPES);
    for (int i = 0; i < Flower.NUM_TYPES; i++) {
      samples.add(new HashMap<Long, Integer>());
    }
    this.approximator = new BeeYieldApproximator(weights, samples);

    switch (id) {
    case 0:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop1;
      initialScouting = true;
      break;
    case 1:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop2;
      initialScouting = true;
      break;
    case 2:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop3;
      initialScouting = true;
      break;
    case 3:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop4;
      initialScouting = true;
      break;
    case 4:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop5;
      initialScouting = true;
      break;
    case 5:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop6;
      initialScouting = true;
      break;
    case 6:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop7;
      initialScouting = true;
      break;
    case 7:
      plan = BeeActionPlanner.StaticExplorationPlans.scout8Loop8;
      initialScouting = true;
      break;
    }
  }


  public Action senseAndAct(Cell[][] view, long timestep) {
    final long timeOfDay = (timestep % Environment.DAY_LENGTH);
    if (timeOfDay < lastTimestep) {
      day++;
    }
    lastTimestep = timeOfDay;
    int energyDelta = -1;
    if (nestCreated) {
      if (hvntComm == null) {
        hvntComm = new int[numBees];
      }
      if (timestep % CommunicateAction.DELAY_PERIOD == 0) {
        addDelays(CommunicateAction.DELAY_PERIOD);
      }
    }
    updateKnowledge(view);
    if (numBees == 0) {
      numBees = getVisibleBees().size(); // assuming they start near each other
    }
    if (previousAction instanceof HarvestAction) {
      int type = ((Flower) getCurrentCell(view)).getType();
      energyDelta = getEnergy() - previousEnergy;
      approximator.addSample(type, timeOfDay, energyDelta);
      // System.out.println("..adding type = " + type +"  time = " + timeOfDay +
      // "  amount = " + energyDelta );
    }
    previousEnergy = getEnergy();

    // If energy level is below a threshold, then rest
    if (getEnergy() < 2) {
      return new RestAction();
    }

    // execute plan
    if (plan != null && !plan.isEmpty()) {
      return plan.remove();
    } else {
      if (planner == null)
        planner = new BeeActionPlanner(cells, flowersOfType, numBees, approximator);
      if (nestCreated) {
        plan = planner.planNewSequence(this, getEnergy(), timeOfDay, nestLocation, getBeesOnMyCell().size(),
            energyDelta, hvntComm, day);
        previousAction = plan.remove();
        return previousAction;
      }
      if (role == Role.QUEEN) {
        initialScouting = false;
        if (nestLocation == null) {
//          System.out.println("squares = " +cells.keySet().size());
          nestLocation = bestLocationForNest(8);
          if (Constants.DEBUG)
            System.out.println(nestLocation);
          return new MoveTowardsAction(nestLocation);
        } else if (getPosition().equals(nestLocation)) { // standing on the nest
          if (getEnergy() > Nest.NEST_COST) {
            nestCreated = true;
            return new CreateNestAction();
          } else {
            return new RestAction();
          }
        } else {
          if (getVisibleBees().size() != numBees) {
            return new RestAction(); // wait for others
          }
          return new MoveTowardsAction(nestLocation);
        }
      } else if (initialScouting) {
        initialScouting = false;
        return new MoveAction(RandomGenerator.getRandomInt(8));
      } else {
        Point nestL = getNestLocation(view);
        if (nestL != null || getEnergy() > 150) {
          if (nestL != null) {
            nestLocation = nestL;
            nestCreated = true;
          }
          // nestCreated == true => start exploitation
          return new RestAction();
        } else {
          return followQueen();
        }
      }
    }

  }

  private void addDelays(int delayPeriod) {
    for (int i = 0; i < numBees; i++) {
      hvntComm[i] += delayPeriod;
    }
  }

  private Point getNestLocation(Cell[][] view) {
    if (nestLocation == null) {
      for (Cell[] cellRow : view) {
        for (Cell cell : cellRow) {
          if (cell instanceof Nest) {
            return cell.getPoint();
          }
        }
      }
    }
    return nestLocation;
  }

  private boolean updateKnowledge(Cell[][] view) {
    // Cell[][] copy = General.deepCopy(view);
    for (Cell[] row : view) {
      for (Cell cell : row) {
        if (cell instanceof Flower) {
          flowersOfType.get(((Flower) cell).getType()).add(cell.getPoint());
        }
        cells.put(cell.getPoint(), cell);
      }
    }
    return true;
  }

  public List<Set<Point>> getFlowersOfType() {
    return flowersOfType;
  }

  public int updateFlowersPositions(List<Set<Point>> flowersOfType) {
    int i = 0;
    int sum = CommunicateAction.MIN_PRICE;
    for (Set<Point> flowerType : this.flowersOfType) {
      Set<Point> s = new HashSet<Point>(flowersOfType.get(i));
      s.removeAll(this.flowersOfType.get(i));
      flowerType.addAll(flowersOfType.get(i++));
      sum += s.size();
    }
    return sum > CommunicateAction.MAX_POSITIONS_PRICE ? CommunicateAction.MAX_POSITIONS_PRICE : sum;
  }

  public int updateFlowersYields(List<Map<Long, Integer>> samples) {
    return approximator.addSamples(samples);
  }

  public void communicate(Bee bee) {
    this.hvntComm[bee.getId()] = 0;
  }

  public Map<Point, Cell> getCells() {
    return cells;
  }

  @Override
  public Bee clone() {
    MyBee newBee = new MyBee(getId());
    newBee.setPosition(getPosition());
    return newBee;
  }

  @Override
  public void addCells(Map<Point, Cell> cells) {
    this.cells.putAll(cells);
  }

  private float[] evaluateCell(Cell cell, int windowSize, int bees) {
    if (!(cell instanceof EmptyCell) || cell instanceof UnseenCell) {
      // unseen sell can be surrounded by many flowers, but it is not probable
      return null;
    } else {
      float[] flowers = new float[Flower.NUM_TYPES + 1];
      int[] flowerNumbers = new int[Flower.NUM_TYPES];
      // int unseenCells = 0;
      int cellX = cell.getPoint().x;
      int cellY = cell.getPoint().y;
      int xMin = cellX - windowSize;
      int xMax = cellX + windowSize;
      int yMin = cellY - windowSize;
      int yMax = cellY + windowSize;
      for (int y = yMin; y <= yMax; y++) {
        for (int x = xMin; x <= xMax; x++) {
          Cell tmpCell = cells.get(new Point(x, y));
          if (tmpCell instanceof Flower) {
            int distance = Math.max(Math.abs(x - cellX), Math.abs(y - cellY));
            Flower f = (Flower) tmpCell;
            // taking bonus for good neighbors (locality)
            flowers[f.getType()] += (f.getNumFlowers() * NUM_OF_FLOWERS_FACOR)
                * (1 - (distance * DISTANCE_FACOR) / windowSize);
            flowerNumbers[f.getType()] += f.getNumFlowers();
          }
          // else if (tmpCell == null || tmpCell instanceof UnseenCell) {
          // unseenCells++;
          // }
        }
      }
      boolean all = true;
      boolean warning = false;
      for (int i = 0; i < Flower.NUM_TYPES; i++) {
        if (flowerNumbers[i] >= bees) {
          if ((flowerNumbers[i] - bees >= 2) && (flowerNumbers[i] - bees < 4)) {
            flowers[i] -= ENOUGH_NUMBER_BONUS;
          } else if (flowerNumbers[i] - bees > 4) {
            flowers[i] -= 1.5 * ENOUGH_NUMBER_BONUS;
          } else {
            flowers[i] += ENOUGH_NUMBER_BONUS;
          }
        } else {
          if (bees - flowerNumbers[i] == 2) {
            if (warning)
              all = false;
            flowers[i] += ENOUGH_NUMBER_BONUS / 6;
            warning = true;
          } else if (bees - flowerNumbers[i] == 1) {
            flowers[i] += ENOUGH_NUMBER_BONUS / 2;
          } else {
            all = false;
          }
        }
      }
      if (all) {
        flowers[Flower.NUM_TYPES] = 1f;
        for (int i = 0; i < Flower.NUM_TYPES; i++) {
          flowers[i] += ENOUGH_NUMBER_OF_ALL_BONUS;
        }
      }
      // flowers[Flower.NUM_TYPES] = unseenCells;
      return flowers;
    }
  }

  private Point bestLocationForNest(int maxWindow) {
    if (role != role.QUEEN) {
      System.err.println("Unauthorized operation (bestLocationForNest called by a non-queen)");
      return null;
    } else {
      General.complementToRectangle(cells);

      float bestSumValueAtAll = 0f;
      Point bestSumPointAtAll = null;
      float[] bestSumResultAtAll = new float[Flower.NUM_TYPES];

      float secondBestSumValueAtAll = 0f;
      Point secondBestSumPointAtAll = null;
      float[] secondBestSumResultAtAll = new float[Flower.NUM_TYPES];

      float bestSumValueAllFlowersArePresentAtAll = 0f;
      Point bestSumPointAllFlowersArePresentAtAll = null;
      float[] bestSumResultAllFlowersArePresentAtAll = new float[Flower.NUM_TYPES];

      float[] bestValueAtAllForFlowerType = new float[Flower.NUM_TYPES];
      Point[] bestPointAtAllForFlowerType = new Point[Flower.NUM_TYPES];
      float[][] bestResultAtAllForFlowerType = new float[Flower.NUM_TYPES][];

      float[] bestSumValueForWindowOfSize = new float[maxWindow + 1];
      Point[] bestSumPointForWindowOfSize = new Point[maxWindow + 1];
      float[][] bestSumResultForWindowOfSize = new float[maxWindow + 1][];

      float[] bestSumValueAllFlowersArePresentForWindowOfSize = new float[maxWindow + 1];
      Point[] bestSumPointAllFlowersArePresentForWindowOfSize = new Point[maxWindow + 1];
      float[][] bestSumResultAllFlowersArePresentForWindowOfSize = new float[maxWindow + 1][];

      float[][] bestValueForWindowOfSizeAndFlowerType = new float[maxWindow + 1][Flower.NUM_TYPES];
      Point[][] bestPointForWindowOfSizeAndFlowerType = new Point[maxWindow + 1][Flower.NUM_TYPES];
      float[][][] bestResultForWindowOfSizeAndFlowerType = new float[maxWindow + 1][Flower.NUM_TYPES][];

      boolean bingo = false;
      for (int i = 3; i <= maxWindow; i++) {
        if (bingo) {
          if (Constants.DEBUG)
            System.out.println("BINGO!");
          break;
        }
        for (Cell c : cells.values()) {
          float[] actualResult = evaluateCell(c, i, Simulation.getSettings().getBeesNum());
          if (actualResult == null) {
            continue;
          }
          if (!bingo)
            bingo = actualResult[Flower.NUM_TYPES] > Constants.PREC;
          float sum = General.sumFloats(actualResult, this.weights);
          boolean fullHouse = General.areAllFlowersPresent(actualResult);
          if (sum > bestSumValueForWindowOfSize[i]) {
            bestSumValueForWindowOfSize[i] = sum;
            bestSumResultForWindowOfSize[i] = actualResult;
            bestSumPointForWindowOfSize[i] = c.getPoint();
          }
          if (fullHouse && sum > bestSumValueAllFlowersArePresentForWindowOfSize[i]) {
            bestSumValueAllFlowersArePresentForWindowOfSize[i] = sum;
            bestSumResultAllFlowersArePresentForWindowOfSize[i] = actualResult;
            bestSumPointAllFlowersArePresentForWindowOfSize[i] = c.getPoint();
          }
          for (int j = 0; j < Flower.NUM_TYPES; j++) {
            if (actualResult[j] > bestValueForWindowOfSizeAndFlowerType[i][j]) {
              bestValueForWindowOfSizeAndFlowerType[i][j] = actualResult[j];
              bestResultForWindowOfSizeAndFlowerType[i][j] = actualResult;
              bestPointForWindowOfSizeAndFlowerType[i][j] = c.getPoint();
            }
          }
        }
        if (bestSumValueForWindowOfSize[i] > bestSumValueAtAll) {
          secondBestSumValueAtAll = bestSumValueAtAll;
          secondBestSumResultAtAll = bestSumResultAtAll;
          secondBestSumPointAtAll = bestSumPointAtAll;
          bestSumValueAtAll = bestSumValueForWindowOfSize[i];
          bestSumResultAtAll = bestSumResultForWindowOfSize[i];
          bestSumPointAtAll = bestSumPointForWindowOfSize[i];
        }
        if (bestSumValueAllFlowersArePresentForWindowOfSize[i] > bestSumValueAllFlowersArePresentAtAll) {
          bestSumValueAllFlowersArePresentAtAll = bestSumValueAllFlowersArePresentForWindowOfSize[i];
          bestSumResultAllFlowersArePresentAtAll = bestSumResultAllFlowersArePresentForWindowOfSize[i];
          bestSumPointAllFlowersArePresentAtAll = bestSumPointAllFlowersArePresentForWindowOfSize[i];
        }
        for (int j = 0; j < Flower.NUM_TYPES; j++) {
          if (bestValueForWindowOfSizeAndFlowerType[i][j] > bestValueAtAllForFlowerType[j]) {
            bestValueAtAllForFlowerType[j] = bestValueForWindowOfSizeAndFlowerType[i][j];
            bestResultAtAllForFlowerType[j] = bestResultForWindowOfSizeAndFlowerType[i][j];
            bestPointAtAllForFlowerType[j] = bestPointForWindowOfSizeAndFlowerType[i][j];
          }
        }
      }
      if (Constants.DEBUG) {
        if (bestSumValueAtAll > Constants.PREC) {
          System.out.println("\nBest sum at all: " + bestSumValueAtAll);
          System.out.println("Point: " + bestSumPointAtAll);
          System.out.println("Types: " + General.floatsToString(bestSumResultAtAll));
        }

        if (secondBestSumValueAtAll > Constants.PREC) {
          System.out.println("\nSecond best sum at all: " + secondBestSumValueAtAll);
          System.out.println("Point: " + secondBestSumPointAtAll);
          System.out.println("Types: " + General.floatsToString(secondBestSumResultAtAll));
        }
        if (bestSumValueAllFlowersArePresentAtAll > Constants.PREC) {
          System.out.println("\nBest sum at all (all flowers are present): " + bestSumValueAllFlowersArePresentAtAll);
          System.out.println("Point: " + bestSumPointAllFlowersArePresentAtAll);
          System.out.println("Types: " + General.floatsToString(bestSumResultAllFlowersArePresentAtAll));
        }
        for (int i = 0; i < Flower.NUM_TYPES; i++) {
          if (bestValueAtAllForFlowerType[i] > Constants.PREC) {
            System.out.println("\nBest value at all for flower of type " + i + ": " + bestValueAtAllForFlowerType[i]);
            System.out.println("Point: " + bestPointAtAllForFlowerType[i]);
            System.out.println("Types: " + General.floatsToString(bestResultAtAllForFlowerType[i]));
          }
        }

        for (int i = 1; i <= maxWindow; i++) {
          System.out.println("\n---- Window size = " + i + " ----\n");
          if (bestSumValueForWindowOfSize[i] > Constants.PREC) {
            System.out.println("\nBest sum: " + bestSumValueForWindowOfSize[i]);
            System.out.println("Point: " + bestSumPointForWindowOfSize[i]);
            System.out.println("Types: " + General.floatsToString(bestSumResultForWindowOfSize[i]));
          }
          if (bestSumValueAllFlowersArePresentForWindowOfSize[i] > Constants.PREC) {
            System.out.println("\nBest sum (all fl. present): " + bestSumValueAllFlowersArePresentForWindowOfSize[i]);
            System.out.println("Point: " + bestSumPointAllFlowersArePresentForWindowOfSize[i]);
            System.out.println("Types: " + General.floatsToString(bestSumResultAllFlowersArePresentForWindowOfSize[i]));
          }
          for (int j = 0; j < Flower.NUM_TYPES; j++) {
            if (bestValueForWindowOfSizeAndFlowerType[i][j] > Constants.PREC) {
              System.out.println("\nBest value for flower of type " + i + ": "
                  + bestValueForWindowOfSizeAndFlowerType[i][j]);
              System.out.println("Point: " + bestPointForWindowOfSizeAndFlowerType[i][j]);
              System.out.println("Types: " + General.floatsToString(bestResultForWindowOfSizeAndFlowerType[i][j]));
            }
          }
        }
      }

      return bestSumPointAtAll;
    }
  }

  private Action followQueen() {
    Point qPos = getQueenPos();
    int distance = howFar(qPos);
    if (qPos == null) {
      if (distance < 3) {
        if (distance == 0)
          return new RestAction();
        // got lost
      }
      return new MoveTowardsAction(lastQueenSeen);
    } else {
      lastQueenSeen = qPos;
      if (distance < 3 && RandomGenerator.getRandomInt(distance + 1) < 1) {
        return new RestAction();
      }
      return new FollowQueenAction(); // TODO: Make general method for following the queen
    }
  }

  private int howFar(Point p) {
    return General.distance(p, getPosition());
  }

  private Point getQueenPos() {
    List<Bee> beesInRange = getVisibleBees2();
    for (Bee beeAux : beesInRange) {
      if (beeAux.getRole() == Role.QUEEN) {
        return beeAux.getPosition();
      }
    }
    return null;
  }

  @Override
  public Role getRole() {
    return role;
  }

  @Override
  public List<Map<Long, Integer>> getSamples() {
    return samples;
  }

  public void setSamples(List<Map<Long, Integer>> samples) {
    this.samples = samples;
  }

}
